package com.googlecode.cswish.oosearch;

import java.util.ArrayList;
import java.util.Collection;
import java.util.IdentityHashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import javax.annotation.Resource;

import org.apache.log4j.Logger;

import com.googlecode.cswish.model.ModelInfo;
import com.googlecode.cswish.model.PropertyInfo;
import com.googlecode.cswish.struts.hibernate.IDynamicModel;
import com.googlecode.cswish.struts.spring.BeanIntrospector;


/**
 * Parse a model object to Query Language (QL)
 * 
 * @author Jerry.Feng Date: 2005-2-28 9:41:51
 * @version $id: $
 */
public class QLParse {
	/**
	 * Logger for this class
	 */
	private static final Logger logger = Logger.getLogger(QLParse.class);
	
	@Resource
	private BeanIntrospector beanIntrospector;
	
	private static final String SELECT = "select ";
	private static final String FROM = " from ";
	private static final String AS = " as ";

	private static final String WHERE = " where ";
	public static final String DEFAULT_ALIAS = "a";

//	private static final int DIALECT = 1;	// 0 -- oracle, 1 -- sql server

	public QLParse() {
	}

	public QLInfo getSelectHQL(Object model, Condition condition) {
		QLInfo hqlInfo = getFromClause(model, condition);
		toSelectHQL(hqlInfo);
		return hqlInfo;
	}
	
	public QLInfo toSelectHQL(QLInfo hqlInfo) {
		hqlInfo.setQl(SELECT + DEFAULT_ALIAS + hqlInfo.getQl());
		return hqlInfo;
	}
			

	public QLInfo getFromClause(Object model, Condition condition) {
		return getFromClause(model, DEFAULT_ALIAS, condition);
	}

    protected QLInfo getFromClause(Object model, String tableAlias, Condition condition) {
    	QLInfo hqlInfo = new QLInfo();
    	StringBuilder fromClause = new StringBuilder();
    	StringBuilder whereClause = new StringBuilder();
        List<Object> paramList = new ArrayList<Object>();
        List<String> paramNameList = new ArrayList<String>();
        
        String className;
        if (model instanceof Map) {
        	className = (String)((Map)model).get(IDynamicModel.ENTITY_NAME);
        } else {
        	className = model.getClass().getName();
        }
        fromClause.append(FROM).append(className).append(' ').append(tableAlias);
    	analyseCondition(model, tableAlias, condition,
    			fromClause, whereClause, paramNameList, paramList,
    			new IdentityHashMap<Object, Object>());
    	
    	// Set ql
    	StringBuilder ql = new StringBuilder();
    	ql.append(fromClause);
    	if (whereClause.length() > 0) {
    		ql.append(WHERE).append('(').append(whereClause).append(')');
    	}
   		hqlInfo.setQl(ql.toString());

    	// Set parameter
    	hqlInfo.setParams(paramList);
    	hqlInfo.setParamNames(paramNameList);

    	// Set page information 
    	hqlInfo.setFirstResult(condition.getOffset());
    	hqlInfo.setMaxResults(condition.getOffset() + condition.getSize());
    	return hqlInfo;
    }

    private String nextTableAlias(String tableAlias) {
    	return tableAlias.replace('.', '_') + "_a";
    }
    
    private void analyseCondition(Object model ,String tableAlias, Condition condition,
    		StringBuilder fromClause, StringBuilder whereClause, List<String> paramNameList, List<Object> paramList,
    		Map<Object, Object> parsedObject) {
    	// make a flag that the model has been analysed
    	parsedObject.put(model, model);

    	String className;
		if (model instanceof IDynamicModel) {
			IDynamicModel dynamicModel = (IDynamicModel) model;
			className = dynamicModel.getEntityName();
		} else if (model instanceof Map) {
			className = (String) ((Map) model).get(IDynamicModel.ENTITY_NAME);

		} else {
			className = model.getClass().getName();
		}
    	// get the public method from cache
    	ModelInfo modelInfo = beanIntrospector.getModelInfo(className);
    	Collection<PropertyInfo> properties = modelInfo.getPersistenceProperties();
    	boolean isFirstOne = true;
    	for (PropertyInfo propertyInfo : properties) {
    		if (propertyInfo.isSimpleObj() && !propertyInfo.isTableColumn()
    				&& !propertyInfo.isSearchInRange()) {
    			continue;
    		}
    		Object value = propertyInfo.getValue(model);

            // check each not empty object
            if (notEmpty(value, propertyInfo.getTypeClass())) {
            	boolean isComplexObj = !propertyInfo.isSimpleObj();
            	if (isComplexObj && parsedObject.get(value) != null) {	// the object has been parsed
            		continue;
            	}
            	
            	int topLength = whereClause.length();
            	if (!isFirstOne) {
            		whereClause.append(condition.getLogicFlag());
            	}
            	
            	String valueName;
                String methodName = propertyInfo.getName();
                valueName = tableAlias + '.' + methodName;
                if (isComplexObj) {
                	whereClause.append('(');
                	int oldLen = whereClause.length();
                	if (value instanceof Collection) {
                		Collection<Object> coll = (Collection<Object>) value;
                		parsedObject.put(coll, coll);
                		String newAlias = nextTableAlias(tableAlias);
                		int fromClauseLen = fromClause.length();
                		fromClause.append(" inner join ").append(valueName).append(' ')
                			.append(newAlias);
                		int theLength = whereClause.length(); 
                		Iterator<Object> iter = coll.iterator();
                		while (iter.hasNext()) {
                			Object element = iter.next();
                			whereClause.append('(');
                			int length = whereClause.length();
                			analyseCondition(element, newAlias, condition, 
                					fromClause, whereClause, paramNameList, paramList, parsedObject);
                			if (length == whereClause.length()) { // no child clause
                				whereClause.deleteCharAt(whereClause.length() - 1);
                			} else {
                				whereClause.append(')');
                				if (iter.hasNext()) {
                    				whereClause.append(Condition.LOGIC_OR);
                    			}
                			}
                		}
                		if (theLength == whereClause.length()) {
                			fromClause.delete(fromClauseLen, fromClause.length());
                		}
                	} else { // model object
                		analyseCondition((Object)value, valueName, condition,
                				fromClause, whereClause, paramNameList, paramList, parsedObject);
                	}
                	
                	if (oldLen == whereClause.length()) {	// no child clause
                		whereClause.delete(topLength, whereClause.length());
                	} else {
                		whereClause.append(')');
                		isFirstOne = false;
                	}
                } else { // simple property
                	if (propertyInfo.isSearchInRange()) {
                		if (valueName.charAt(valueName.length() - 1) == '2') {
                			whereClause.append(valueName.substring(0, valueName.length() - 1));
                		} else {
                			whereClause.append(valueName);
                		}
                	} else {
                		whereClause.append(valueName);
                	}
                	
                	String cmpFlag = getCmpFlag(propertyInfo, condition.getCompareFlag(), propertyInfo.getType());
                    whereClause.append(cmpFlag);
                    whereClause.append('?');
                    
                    Object cmpValue = getCmpValue(cmpFlag, value); 
                    paramNameList.add(valueName);
                    paramList.add(cmpValue);
                    isFirstOne = false;
                }
            }
        }
    }
    
    private boolean notEmpty(Object value, Class<?> type) {
    	boolean hasValue = false;
    	
    	if (value != null) {
    		if (type != null && type.isPrimitive()) {
        		if (boolean.class == type) {
        			hasValue = ((Boolean)value).booleanValue();
        		} else {
        			int i = Integer.parseInt(value.toString());
        			hasValue = (i != 0);
        		}
        	} else {
	    		if (value instanceof String) {
	    			if (((String)value).length() > 0) {
	    				hasValue = true;
	    			}
	    		} else if (value instanceof Collection) {
	    			try {
		    			if (((Collection)value).size() > 0) {//avoid the hibernate lazy load exception
		    				hasValue = true;
		    			}
	    			} catch (Throwable t) {
	    			}
	    		} else {
	    			hasValue = true;
	    		}
        	}
    	}
		return hasValue;
	}

//	/**
//     * FIXME: use >= and <=, refactor the input parameter
//     * 
//     * convert to char type from the other types 
//     */
//    private String getCharConvertSQL(String fieldName, boolean isDate) {
//    	StringBuilder sql = new StringBuilder();
//    	if (DIALECT == 0) {	// oracle
//	    	if (isDate) {
//				sql.append("to_char(").append(fieldName)
//					.append(",'YYYY/MM/DD')");
//			} else {
//				sql.append("to_char(").append(fieldName).append(')');
//			}
//    	} else if (DIALECT == 1) {	// sql server
//    		if (isDate) {
//    			// 110 -- mm/dd/yyyy, 111 -- yyyy/mm/dd,	112 -- yyyymmdd
//    			sql.append("convert(char,").append(fieldName).append(",111)");
//    		} else {
//    			sql.append("convert(char,").append(fieldName).append(')');
//    		}	
//    	}
//    	return sql.toString();
//    }
    
    private String getCmpFlag(PropertyInfo propertyInfo, String cmpFlag, String type) {
    	if (Condition.COMPARE_AUTO.equals(cmpFlag)) {
    		if (String.class.getName().equals(type)) {
    			// TODO: use hibernate search to do the 'like' match
    			//return Condition.COMPARE_LIKE;
    			return Condition.COMPARE_EQUAL;
    		} else {
    			if (propertyInfo.isSearchInRange()) {
    				if (propertyInfo.getName().endsWith("2")) {
    					return "<";
    				} else {
    					return ">";
    				}
    			} else {
    				return Condition.COMPARE_EQUAL;
    			}
    		}
    	} else {
    		return cmpFlag;
    	}
    }
    
    private Object getCmpValue(String cmpFlag, Object value) {
    	if (cmpFlag == Condition.COMPARE_LIKE) {
   			value = "%" + value + "%";
        }
    	
    	return value;
    }
}
